<?php
/*----------------------------------------------*\
| ErrorHandler class for PHP development         |
| written by      : Gyozo Papp @: gerzson@php.net|
| VERSION-CTRL    : 2.0a5                        |
| last modified   : 2002.08.28.                  |
\*----------------------------------------------*/
// current TODO:
// 1. COLLECT mail, header('Location:'); registered shutdown functions  invoked???
// (task: examine whether such a function invoked or not, I doubt it, because calling
// exit() needed after header('location'); terminates the execution immediately
// Now, I checked it. It  works, hmmm...
// 2. SESSION support (work around when header('Location:') clears the CONSOLE, etc.
// I examined the task, and it will be a hard work to develop a useable version
// without limiting the current capabilities or restricting its usage. IMHO, it's not
// an option that the half of the features can be used only in conventional version,
// and the other must be set in a different way if you want to use SESSION.
// 3. bugfix: where to store encrypt and collect settings within the object?
// (what private property in LOGGING sections or separate one ??? ~> decision needed)
// 4. "I have a little problem with class trap error from function with @."
// (what is the exact case with errors muted by @ operator)
/*---------------------------------*\
|  CONSTANTs  used by ErrorHandler  |
\*---------------------------------*/
define('SYSTEM_LOG', 0);
define('MAIL_LOG',   1);
define('FILE_LOG',   3);
define('E_USER_ALL',    E_USER_NOTICE | E_USER_WARNING | E_USER_ERROR);
define('E_NOTICE_ALL',  E_NOTICE | E_USER_NOTICE);
define('E_WARNING_ALL', E_WARNING | E_USER_WARNING | E_CORE_WARNING | E_COMPILE_WARNING);
define('E_ERROR_ALL',   E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR);
define('E_NOTICE_NONE', E_ALL & ~E_NOTICE_ALL);
define('E_DEBUG',       0x10000000);
define('E_VERY_ALL',    E_ERROR_ALL | E_WARNING_ALL | E_NOTICE_ALL | E_DEBUG);
define('ERRORHANDLER_CONSOLE','
<script type="text/javascript" language="JavaScript"><!--
ErrorHandler = window.open("", "ErrorHandlerConsole","resizable=yes,scrollbars=yes,directories=no,location=no,menubar=no,status=no,toolbar=no");
ErrorHandler.document.open();
ErrorHandler.document.writeln("<html><head><title>ErrorHandler Console: %s<\\/title><\\/head><body>");
ErrorHandler.document.writeln("<form action=\'http://www.web-cp.net/report.php\' method=\'post\'><input type=\'submit\' value=\'Send Report\'> <font size=-1>(this submits a scrubbed report to the official web-cp project for bug discovery and tracking)</font><br><hr>");
%s
ErrorHandler.document.writeln("<\\/form><\\/body><\\/html>");
ErrorHandler.document.close();
//--></script>
');
/*-----------------------------------------------------------*\
|                   BEGIN CONFIGURATION SECTION               |
|  Here you can set a few settings outside of the .ini file   |
| These settings are mostly used before parsing of .ini file  |
\*-----------------------------------------------------------*/
// small HTML code sent to the client,
// if client-side redirection is needed according to the REPLACE directives
define('ERRORHANDLER_REPLACE_CLIENT', '
<script type="text/javascript" language="JavaScript"><!--
setTimeout("window.location.href = \'%1$s\'", 3000);
//--></script>
:::ERROR!::: You will be redirected automatically in 3 seconds,
otherwise <a href="%1$s">click here</a>.
');
// .ini file, if not supplied to the constructor
define('ERRORHANDLER_INI',        dirname(__FILE__).'/ErrorHandler.ini');
// where to send startup failures (defualt log type and destination)
define('ERRORHANDLER_LOG_TYPE',   FILE_LOG);
define('ERRORHANDLER_LOG_TARGET', 'ErrorHandler.log');
// timestamp format
define('ERRORHANDLER_DATE_FORMAT','[Y-m-d H:i:s]');
/*-----------------------------------------------------------*\
|                 END CONFIGURATION SECTION                   |
|  There should be no need to touch anything below this line. |
\*-----------------------------------------------------------*/
class ErrorHandler
{
	/*
	 * PUBLIC properties
	 */
	var $CUSTOM     = '';
	var $SILENT     = FALSE;
	// possible ERRORHANDLER_LOCKED constant
	/*
	 * PRIVATE properties -- DO NOT write directly these ones!
	 * Use the set_*() methods instead!
	 */
	// each property with value of NULL will be an array, but
	// it is advised to fill them within the constructor, though
	// using array() here works as well.
	// these properties can be LOCKED
	var $_SECTION   = NULL; // it should be a private static array!
	var $_LEVEL     = NULL; // array of "report level"s
	var $_ALTDLOG   = NULL; // array of registered file log information
	var $_CONTEXT   = NULL; // array of CONTEXT settings
	var $_SOURCE    = NULL; // array of SOURCE settings
	var $_LOGGING   = NULL;	// array of LOGGING information
	var $_REPLACE   = NULL; // array of FAULT/REPLACE report
	// the following properties are not affected by the LOCKED directive
	var $_TRAP      = NULL; // error trap stack
	var $_TRAP_LEVEL= 0;    // error trap stack
	var $_CONSOLE   = '';   // CONSOLE window code
	var $_COLLECT   = '';   // COLLECT mail message
	function ErrorHandler( $inifile = ERRORHANDLER_INI ){
		// names of the built-in sections to be recognized
		$this->_SECTION = array('CONTEXT', 'LOGGING', 'REPLACE', 'SOURCE');
		$this->_LEVEL   = array('ALL' => 0, 'DEFAULT' => 0, 'CONTEXT' => 0, 'LOGGING' => 0, 'REPLACE' => 0, 'SOURCE' => 0);
		$this->_TRAP    = array();
		$this->_escchrs = array("\t" => '\\t', "\n" => '\\n', "\r" => '\\r', '\\' => '&#092;', "'" => '&#39;', '</' => '<\\/');
		// setup from the supplied ini file
		if ( $this->load_ini($inifile) ){
			// start output buffering (CONSOLE window and REPLACE PAGE needs it)
			// client side redirection makes it impossible to raise a CONSOLE window,
			// so it's needless to start output buffering
			// (but ErrorHandler not turns it OFF, it's not her responsibility at all)
			if ( strcasecmp($this->_REPLACE['redirect'], 'client') != 0 ){
				ob_start(array(&$this, '_console'));
			}
			// E_ERRORs cannot be handled by ErrorHandler under some circumstances.
			// (PHP as PWS CGI can handle, PHP as Apache module can not.)
			// Thus, "display_errors" php.ini setting is enabled if no SILENT mode.
			ini_set('display_errors', !$this->SILENT);
		} else {
			// some error occured during start up ~> abort
			error_log($inifile.': configuration error, script terminated.', ERRORHANDLER_LOG_TYPE, ERRORHANDLER_LOG_TARGET);
			return;
		}
	}
	/*
	 *	CONFIGURATION MANIPULATING METHODS
	 */
	function load_ini( $inifile ){
		if ( !file_exists($inifile) ){
			return FALSE;
		}
		$ini = parse_ini_file($inifile, TRUE);
		foreach ( $ini as $section => $setting ){
			if ( is_array($setting) ){
				// process SECTION
				foreach ( $setting as $flag => $value ){
					// if flag or value can be a constant, use that constant intead
					// greedy method ~> I'm afraid it should be changed shortly
					if ( defined($flag) ){
						$flag = constant($flag);
					}
					if ( in_array($section, $this->_SECTION) ){
						// built-in SECTION directives
						if ( !strcasecmp($flag, 'level') ){
							$this->set($section, $this->_intlevel($value));
						} else {
							 call_user_func(array(&$this, 'set_'.$section), $flag, $value); // $this->{"set_$section"}($flag, $value);
						}
					} else {
						// not built-in SECTION directives ~> ADLTLOG file-registering
						$this->set_altdlog($section, $flag, $value);
					}
				}
			} else {
				// common GLOBAL settings
				switch ( $flag = strtoupper($section) ){
				case 'LEVEL' :
					$this->set('DEFAULT', $this->_intlevel($setting));
					break;
				case 'CUSTOM':
					$this->CUSTOM  = is_callable($setting) ? $setting : NULL;
					break;
				case 'LOCKED': case 'SILENT':
					$this->{$flag} = (bool)$setting;
				default:
					break;
				}
			}
		}
		// if global LEVEL is not specified, try to get from php.ini
		if ( !isset($this->_LEVEL['DEFAULT']) ){
			$this->set('DEFAULT', ini_get('error_reporting'));
		}
		
		return TRUE;
	}
	// restore default php.ini setting possibly affected by ErrorHandler
	// this is a quick hack, it restores the values available at script
	// start-up not the values which is available ErrorHandler's startup
	function restore() {
		restore_error_handler();
		ini_restore('error_log');
		ini_restore('log_errors');
		ini_restore('display_errors');
	}
	function set( $section, $level ){
		static $prev_level = null;
		if ( defined('ERRORHANDLER_LOCKED') || (!in_array($section, $this->_SECTION) && strcmp($section, 'DEFAULT')) ){
			return FALSE;
		}
		// "quick switch" (ON/OFF), isset(...) indicates that it can be used after load_ini() only
		if ( is_bool($level) && isset($this->_LEVEL[$section]) ){
			if ( $level ) {
        // turn ON report (specified in $section)
        // attempt to  restore previous level, otherwise use DEFAULT
				$prev_level[$section]   = $this->_LEVEL[$section];
				$this->_LEVEL[$section] = !empty($prev_level[$section])?$prev_level[$section]:$this->_LEVEL['DEFAULT'];
			}
			else if ( !empty($this->_LEVEL[$section]) ){
				// turn OFF report (specified in $section) if possible (level > 0)
				$prev_level[$section]   = $this->_LEVEL[$section];
				$this->_LEVEL[$section] = 0;
			}
		} else { // general purpose switch / change the actual report level
			$level = $this->_intlevel($level);
			$prev_level[$section] = $this->_LEVEL[$section];
			$this->_LEVEL['ALL'] |= $this->_LEVEL[$section] = $level;
			if ( strcasecmp($section, 'DEFAULT') == 0 ){
				foreach ( $this->_SECTION as $s ){
					if ( empty($this->_LEVEL[$s]) ){
            # TODO: isset -- empty -- === 0 mizeria mi van, ha 0-ra van allitva, akkor felulirja???
            # pedig lehet, hogy direkt ki akarta kapcsolni
						$this->_LEVEL[$s] = $this->_LEVEL['DEFAULT'];
					}
				}
			}
			return $prev_level[$section];
		}
	}
	function set_altdlog( $file_name, $flag, $value = null ){
		if ( $flag === FALSE ){
			// clears previously registered $file_name
			unset($this->_ALTDLOG[crc32(realpath($file_name))]);
			return TRUE;
		}
		else if ( isset($value) ){
			$file_name = crc32(realpath($file_name));
			if ( $file_name ){
				switch ( $flag ){
				case 'level':
					@$this->_LEVEL['ALL'] |= $this->_LEVEL['ALTDLOG'][$file_name] = $this->_intlevel($value);
					break;
				case MAIL_LOG:
				case FILE_LOG:
				case SYSTEM_LOG:
					$this->_ALTDLOG[$file_name][$flag] = $value;
				default: break;
				}
				return TRUE;
			} else {
				return FALSE;
			}
		}
	}
	function set_context( $flag, $value = null ){
		switch ( $flag = strtolower($flag) ){
		case 'level':
			@$this->_LEVEL['ALL'] |= $this->_LEVEL['CONTEXT'] = $this->_intlevel($value);
			break;
		case 'strict':
		case 'depth':
			$this->_CONTEXT[$flag] = intval($value); break;
		case 'exclude':
			// if the 2nd arg is empty it clears the actual exclude list
			if ( empty($value) || empty($this->_CONTEXT['exclude']) ){
				$this->_CONTEXT['exclude'] = array();
			}
			for ( $i = 1; $i < func_num_args(); $i++ ){
				$arg = func_get_arg($i);
				if ( is_string($arg) ){
					$this->_CONTEXT['exclude'] =
						array_merge($this->_CONTEXT['exclude'], preg_split('/[\W]+/', $arg, -1, PREG_SPLIT_NO_EMPTY));
				}
				else if ( is_array($arg) ){
					$this->_CONTEXT['exclude'] = array_merge($this->_CONTEXT['exclude'], $arg);
				}
			}
		default:
		}
	}
	function set_logging( $type, $target ){
		static $collect = FALSE;
		switch ( $type ){
			case 'level':
				@$this->_LEVEL['ALL'] |= $this->_LEVEL['LOGGING'] = $this->_intlevel($target);
				break;
			case SYSTEM_LOG: ini_set('error_log', 'syslog'); break;
			case FILE_LOG:   ini_set('error_log', $target); break;
			case MAIL_LOG:   ini_set('error_log', ERRORHANDLER_LOG_TARGET); break;
			case 'encrypt': break; // TODO!!!
			case 'collect':
				if ( $target && !$collect ) {
					return $collect = register_shutdown_function(array(&$this, '_collect'));
				}
			default: return; // Beware, this is an exit point!
		}
		ini_set('log_errors', TRUE);
		$this->_LOGGING[$type] = $target;
	}
	function set_replace( $flag, $value ){
		switch ( $flag = strtolower($flag) ){
		case 'level':
			@$this->_LEVEL['ALL'] |= $this->_LEVEL['REPLACE'] = $this->_intlevel($value);
			break;
		case 'page':
		case 'redirect':
			$this->_REPLACE[$flag] = $value; break;
		default:
		}
	}
	function set_source( $flag, $value ){
		switch ( $flag = strtolower($flag) ){
		case 'level':
			$this->_LEVEL['ALL'] |= $this->_LEVEL['SOURCE'] = $this->_intlevel($value);
			break;
		case 'lines':
		case 'block':
			$this->_SOURCE[$flag] = intval($value); break;
		default:
		}
	}
	function trap( $level = TRUE ){
		if ( $level === TRUE ){
			$this->_TRAP_LEVEL = $this->_LEVEL['DEFAULT'];
		} else {
			$this->_TRAP_LEVEL = $level;
		}
		$this->_TRAP = array();
	}
	function is_trapped( $level = null ){
		if ( empty($level) ){
			$level = $this->_TRAP_LEVEL;
		}
		$size = count($this->_TRAP);
		for ( $i = 0 ; $i < $size; $i++ ){
			if ( $this->_TRAP[$i]['level'] & $level ){
				return $this->_TRAP[$i];
			}
		}
		return FALSE;
	}
	/*
	 * USER-TRIGGERED REPORTING METHODS
	 */
	// Oh men, how do I get rid of this long argument list, any suggestion?
	function debug( $variable, $name = '*unknown*', $file_name = '*unspecified* file', $line_no = '*unknown*' ){
		$this->_lockreset();
		$message = $this->_message(E_DEBUG, $file_name, $line_no, '');
		$context = $this->_sprint_var($variable, $name, 0);
		$this->_logall(E_DEBUG, $file_name, $message . $context . "\n");
		$this->_output($message, '', $context);
	}
	function log( $message, $file_name = '*unspecified* file', $line_no = '*unknown*', $type = null, $target = null ){
		$this->_lockreset();
		$message = $this->_message(E_LOG, $file_name, $line_no, $message);
		// check if $type and $target points to a valid log destination
		if ( is_null($type) ){
			// $this->_LEVEL['ALL'] forces logging
			$this->_logall($this->_LEVEL['ALL'], $file_name, $message);
		} else {
			// log to the supplied destination
			$this->_log($message, array($type, $target));
		}
	}
	/*
	 * PRIVATE methods -- DO NOT CALL from outside $this class!
	 *
	 */
	// _console will be invoked when ob_end_flush() is called, or when the
	// output buffer is flushed to the browser at the end of the request.
	function _console( $output ){
		global $_SERVER;
		$this->_lockreset();
		if ( empty($this->_CONSOLE) || $this->SILENT ){
			return $output;
		}
		$history = sprintf(ERRORHANDLER_CONSOLE, $_SERVER['SCRIPT_NAME'], $this->_CONSOLE);
		$return  = preg_replace('!(<head(?(?=\s)[^>]*)>)!i', '$1 '.$history, $output, 1);
		if ( strlen($return) > strlen($output) ){
			return $return;
		} else {
			return $history.$output;
		}
	}
	// create CONTEXT report
	function _context( $level, &$var_context, $source ){
		// if CONTEXT report should be generated
		if ( !($this->_LEVEL['CONTEXT'] & $level) ){
			return '';
		}
		$context   = sprintf("`--> CONTEXT REPORT (strict: %s, filtered: %s)\n",
			$this->_CONTEXT['strict']?'yes':'no', $this->_CONTEXT['exclude']?'yes':'no');
		$var_names = array_keys($var_context);
		if ( !empty($this->_CONTEXT['strict']) ){
			if ( !empty($source) ) {
				$strict = array();
				// normal variables
				if ( preg_match_all('!({)? \$ (?(1) ([\'"])? | ({[\'"])?)  # simple curly syntax
					(?'.'>([_a-z\x7f-\xff][\w\x7f-\xff]*)) # character class taken from PHP Manual
					(?(1) (?(2)$2)} | (?(2)))!ix', $source, $match) ) {
					$strict = array_unique($match[4]);
				}
				// variable variables, it's a crude hack (can be fooled quite easily)
				// now, simple variables found ~> check each variable of type string
				// if there is any variable named as its value, respectively
				foreach ( $strict as $value ){
					if ( is_string($context[$value]) ){
						if ( in_array($context[$value], $var_names) && !in_array($context[$value], $strict) ){
							$strict []= $context[$value];
						}
					}
				}
				$var_names = array_intersect($var_names, $strict);
			} else {
				return $context . "* no source to extract variables. *\n";
			}
		}
		if ( empty($this->_CONTEXT['exclude']) ){
			foreach ( $var_names as $name ) {
				$context .= $this->_sprint_var($var_context[$name], $name, 0);
			}
		}
		else {
			settype($this->_CONTEXT['exclude'], 'array');
			foreach ( $var_names as $name ) {
				if ( !in_array($name, $this->_CONTEXT['exclude']) ){
					$context .= $this->_sprint_var($var_context[$name], $name, 0);
				}
			}
		}
		return $context;
	}
	// _handle_error() method -- invoked by Lookup_ErrorHandler
	function _handle_error( $level, $err_str, $file_name, $line_no, $context ){
		global $_SERVER, $cfg;
		// if this error should be trapped
		if ( $this->_TRAP_LEVEL & $level ) {
			$this->_TRAP[] = compact('level', 'err_str', 'file_name', 'line_no');
			return;
		}
		// restore locked settings if needed
		$this->_lockreset();
		// if any kind of report should be generated from this error
		if ( !($this->_LEVEL['ALL'] & $level) ){
			return FALSE;
		}
		// split error messages emitted by run-time generated codes
		// (e.g. create_function(), eval(), etc. )
		if ( preg_match('!^(.*)\((\d+)\) : (.*)$!U', $file_name, $match) ) {
			$file_name = $match[1];
			$line_no   = $match[2];
			$err_str   = $err_str.' in '.$match[3];
		}
		// CUSTOM callback produces the error messages or the built-in or not?
		if ( !empty($this->CUSTOM) && is_callable($this->CUSTOM) ){
			$message = call_user_func($this->CUSTOM, $err_str, $level, $file_name, $line_no);
		} else {
			$message = $this->_message($level, $file_name, $line_no, $err_str);
		}
		$srch[] = '"';
		$rplc[] = '%QOT%';
		$srch[] = '&#39;';
		$rplc[] = '%QOT%';
		$source  = $this->_source($level, $file_name, $line_no);
		$fields = '<input type=hidden name="level[]" value="'.$level.'">';
		$fields .= '<input type=hidden name="errstring[]" value="'.$err_str.'">';
		$fields .= '<input type=hidden name="filename[]" value="'.$file_name.'">';
		$fields .= '<input type=hidden name="line[]" value="'.$line_no.'">';
		$fields .= '<input type=hidden name="webcpver[]" value="'.$cfg['webcp'].'">';
		//$fields .= '<input type=hidden name="source[]" value="'.str_replace($srch, $rplc, $source).'">';
		$context = $this->_context($level, $context, $source);
		//$fields .= '<input type=hidden name="context[]" value="'.str_replace("\"", "'", $context).'">';
		// send log to each destination
		$this->_logall($level, $file_name, $message."\n(request: ".$_SERVER['SCRIPT_NAME'].")\n".$source.$context."\n");
		// if the actual error hits the REPLACE error level
		if ( $this->_LEVEL['REPLACE'] & $level ){
			$this->_replace();
		}
		else if ( !$this->SILENT ){
			$this->_output($message.$fields, $source, $context);
		}
	}
	// creates own error message
	function _message( $level, $file_name, $line_no, $err_str ){
		$message = date(ERRORHANDLER_DATE_FORMAT);
		if ( $level & E_ERROR_ALL ) {
			$message .= ' ERROR ';
		}
		else if ( $level & E_WARNING_ALL ) {
			$message .= ' WARNING ';
		}
		else if ( $level & E_NOTICE_ALL ) {
			$message .= ' NOTICE ';
		}
		else if ( $level & E_DEBUG ){
			$message .= ' DEBUG information ';
		}
		else if ( $level & E_LOG ){
			$message .= ' LOG message ';
		}
		// if SOURCE report is required, $file_name and $line_no not appended
		if ( $this->_LEVEL['SOURCE'] & $level ){
			$message .= sprintf("(0x%02x) %s\n", $level, $err_str);
		} else {
			$message .= sprintf("(0x%02x) in %s on line %s\n%s\n", $level, $file_name, $line_no, $err_str);
		}
		return $message;
	}
	// underlying LOGGING mechanism
	function _log( $logmsg, $logging ){
		// traverse $logging destination ~> log to each destination
		foreach ( $logging as $type => $target ) {
			// is the current destination is a mailbox and mail encryption is ON?
			if ( $type == MAIL_LOG && is_callable(@$this->_LOGGING['encrypt']) ){
				// call user defined function to encrypt $logmsg
				$logmsg = call_user_func($this->_LOGGING['encrypt'], $logmsg);
				// it is allowed to return with an array, too
				if ( is_array($logmsg) ){
					// 1st element (at index 0) is the enrypted $logmsg,
					// 2nd element (at index 1) is the additional headers
					@error_log($logmsg[0], $type, $target, $logmsg[1]);
				} else {
					// normal mail will be sent
					@error_log($logmsg, $type, $target);
				}
			} else {
				@error_log($logmsg, $type, $target);
			}
		}
	}
	function _logall( $level, $file_name, $logstr ){
		if ( $this->_LEVEL['LOGGING'] & $level ){
			$this->_log($logstr, $this->_LOGGING);
		}
		$key = crc32($file_name);
		// if the actual file is registered or not
		if ( !empty($this->_ALTDLOG[$key]) ){
			// $level must match against 'level' if specified, otherwise DEFAULT level
			if ( (isset($this->_LEVEL['ALTDLOG'][$key]) && ($this->_LEVEL['ALTDLOG'][$key] & $level)) ||
				($this->_LEVEL['DEFAULT'] & $level) ){
				$this->_log($logstr, $this->_ALTDLOG[$key]);
			}
		}
	}
	//
	function _output( $message, $source, $context ){
		$report = ini_get('error_prepend_string').nl2br($message).ini_get('error_append_string');
		if ( !empty($source) ){
			// opens a buffer to gather formatted $message, $source and $context
			ob_start();
				@highlight_string($source);
				$report .= ob_get_contents();
			ob_end_clean();
		}
		$report = '<nobr>'.str_replace("\n", "</nobr><nobr>", $report);
		if ( !empty($context) ){
			$report .= '<pre>'.htmlspecialchars($context, ENT_QUOTES)."</pre>\r\n";
		}
		// append reports to the CONSOLE
		$this->_CONSOLE .= sprintf("ErrorHandler.document.writeln('%s<hr \/>');\r\n", strtr($report, $this->_escchrs));
	}
	// handles REPLACE page
	function _replace(){
		switch ( $this->_REPLACE['redirect'] ){
		case 'server':
			if ( !headers_sent() ){
				header('Location:'. $this->_REPLACE['page']);
				return;
			}
		// attempt client-side redirection
		case 'client':
			ob_end_flush();
			printf(ERRORHANDLER_REPLACE_CLIENT, $this->_REPLACE['page']);
			return;
		default:
			ob_end_clean();
			if ( file_exists($this->_REPLACE['page']) ){
				include($this->_REPLACE['page']);
			} else {
				$this->_log($this->_REPLACE['page']." not found\n", $this->_LOGGING);
			}
			return;
		}
	}
	// creates SOURCE report
	// extract from the source file the line which generates the error
	function _source( $level, $file_name, $line_no ){
		// if SOURCE report should be generated
		if ( ($this->_LEVEL['SOURCE'] & $level) == 0 ){
			return '';
		}
		$header = sprintf("`--> SOURCE REPORT from %s around line %d", $file_name, $line_no);
		if ($line_no == 0){
			return $header."\n";
		}
		$file = file($file_name, 1);
		while ( $this->_SOURCE['block'] ){ // in fact, it's not a really loop (exit point at the end)
			for ( $i = $line_no-1, $d = -1, $block = 0; ($d == -1 && $i) || ($block > 0 && $i < count($file)); $i += $d ){
				if ( preg_match('/(?: function\s+\w+\s*\(.*\) | (?:(?:else)?if|switch|while|declare) \s*\(.+\) | else | do
					| for\s*\(.*;.*;.*\) | foreach\s*\(.* as\s+\$\w+(?:\s*=\>\s*\$\w+)?\s*\) ) \s*{ /Sxi', $file[$i]) ){
					if (  ++$block > 0 ){
						$d = +1;
						$j = $i;
						$i = $line_no-1;
					}
				} else {
					preg_match_all('!(?'.'>[^/#}]*) (?(?=.) ( (?://) | [}#] ) )!x', $file[$i], $match);
					foreach( $match[1] as $m ){
						if ( $m == '}' ){ $block--; } else { break; }
					}
				}
			}
			// not found starting block
			if ( $d == -1 && !$i ){
				break; // that's why we needed fake loop above!
			}
			$source = join('', array_slice($file, $j, $i-$j));
			return sprintf("%s (%d-%d)\n%s", $header, $j, $i, $this->_phpscope($source));
		}
		$context = $this->_SOURCE['lines'] >= 0 ? $this->_SOURCE['lines'] : 1;
		$offset  = $line_no-1 >= $context ? $line_no-1 - $context : 0;
		$count   = 2*$context+1;
		$source  = array_slice($file, $offset, $count);
		$count   = count($source);
		$source  = join('', $source);
		if (!isset($fields)) { $fields = ''; }
		return sprintf("%s %s (%d-%d)\n%s", $header, $fields, $offset+1, $offset+$count, $this->_phpscope($source));
	}
	/*
	 * Auxiliary private functions for different report layouts
	 */
  // intelligent integer-casting function
	function _intlevel($value){
		$value = trim($value);
		if ( is_string($value) ){
			if ( defined($value) ){
				$value = constant($value);
			}
			else if ( preg_match('!^0x[\da-f]+!i', $value) ){
				$value = hexdec($value);
			}
			else if ( preg_match('!^0[0-7]+!i', $value) ){
				$value = octdec($value);
			}
		}
		return intval($value);
	}
	// restore ErrorHandlers locked settings
	function _lockreset(){
		if ( defined('ERRORHANDLER_LOCKED') ){
			foreach ( @unserialize(ERRORHANDLER_LOCKED) as $property => $value ){
				$this->$property = $value;
			}
		}
	}
	function _phpscope( $source ){
		$opentag  = '<\?php';
		$closetag = '\?'.'>';
		if (ini_get('asp_tags')) {
			$opentag  .= '|<%';
			$closetag .= '|%>';
		}
		if (ini_get('short_open_tag')) {
			$opentag  .= '|<\?';
		}
		$closepos = preg_match("!($closetag)!", $source, $match) ? strpos($source, $match[0]): FALSE;
		if (preg_match("!($opentag)!", $source, $match)) {
			$openpos1 = strpos($source, $match[1]);
			$openpos2 = strpos($source, $match[1], $closepos);
			if ($openpos1 > $closepos) {
				$source = "<?php\n".$source;
			}
		} else {
			$openpos1 = $openpos2 = FALSE;
			$source   = "<?php\n".$source;
		}
		if ($openpos2 > $closepos || $closepos === FALSE) {
			$source  .= '#*/?'.">\n";
		}
		return $source;
	}
	function _sprint_var( $var, $name, $level ){
		$pad  = str_pad('', 2*$level, ' ');
		if ( is_array($var) || is_object($var) ) {
			if ( is_object($var) ) {
				$buffer = sprintf("$pad%s is an object(%s) ", $level ? "'$name'" : $name, get_class($var));
				$var	= get_object_vars($var);
			} else {
				$buffer = sprintf("$pad%s is an array[%d] ",  $level ? "'$name'" : $name, count($var));
			}
			if($level < $this->_CONTEXT['depth']) {
				$buffer .= "{\n";
				foreach ( $var as $name => $value ) {
					$buffer .= $this->_sprint_var($value, $name, $level+1);
				}
				$buffer .= $pad."}\n";
			} else {
				return $buffer . "* max. depth reached *\n";
			}
		} else {
			// simple types
			if ( is_bool($var) ){
				$var = $var ? 'TRUE' : 'FALSE';
			}
			else if ( is_int($var) ){
				$var = sprintf('%1$d = 0x%1$02x = 0%1$02o = %1$04b', $var);
			}
			else if ( is_resource($var) ){
				$var = strval($var) .' ('. get_resource_type($var).')';
			}
			else if ( is_null($var) ){
				$var = 'NULL';
			}
			else if ( is_string($var) ){
				if ( strlen($var) > 120 && stristr(@$GLOBALS['HTTP_SERVER_VARS']['HTTP_USER_AGENT'], 'win') ){
					// Opera on Windows (and maybe some other browsers) crashes and
					// freezes Win'9x box, if line (made up of chars between two "\n") is too long :((
					$var = substr($var, 0, 120) . '... (* truncated *)';
				} else {
					$var = "'$var'";
				}
			}
			// no preformatting to doubles
			$buffer  = sprintf("$pad%s\t=%s %s\n", is_integer($name) || !$level ? $name : "'$name'", $level?'>':'', $var);
		}
		return $buffer;
	}
}
/*----------------------------------------------*\
|             AutoLaunch ErrorHandler            |
\*----------------------------------------------*/
/*
 * Lookup_ErrorHandler searches the first instance of class ErrorHandler
 * and calls its HandleError method, or return its name.
 */
function Lookup_ErrorHandler( ){
	static $EH_name = '', $OK = FALSE;
	if ( empty($EH_name) ) {
		foreach ( array_keys($GLOBALS) as $EH_name ) {
			if ( is_object($GLOBALS[$EH_name]) && 'errorhandler' == get_class($GLOBALS[$EH_name]) ) {
				$OK = TRUE;
				break;
			}
		}
	}
	// ErrorHandler not found
	if ( !$OK ){
		$EH_name = '';
	}
	if ( func_num_args() > 0 ){
		$args  = func_get_args();
		if ( $OK ){
			$error =& $GLOBALS[$EH_name];
			call_user_func_array(array(&$error, '_handle_error'), $args);
		} else {
			restore_error_handler();
			if ( function_exists(OLD_ERROR_HANDLER) ){
				call_user_func_array(OLD_ERROR_HANDLER, $args);
			}
			return FALSE;
		}
	} else {
		return $OK ? $EH_name : FALSE;
	}
}
// checks PHP version
if ( strcmp('4.0.6', phpversion()) > 0 ){
	error_log('Unable to launch ErrorHandler, PHP 4.0.6 or higher needed.', ERRORHANDLER_LOG_TARGET, ERRORHANDLER_DEFAULT_LOG_TYPE);
	return;
}
define ('OLD_ERROR_HANDLER', set_error_handler('Lookup_ErrorHandler')); // comment out to debug ErrorHandler
ini_set('display_errors', FALSE);
?>
